

using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Management;
using Microsoft.WindowsAzure.Management.Compute;
using Microsoft.WindowsAzure.Management.Compute.Models;
using Microsoft.WindowsAzure.Management.Models;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

namespace vmAutoScaler
{

    public class vmAutoScaler : RoleEntryPoint
    {
        // The task queues
        private CloudQueue taskqueue;
        private CloudQueue highq;
        private CloudQueue mediumq;
        private CloudQueue lowq;
        // The idle node queue
        private CloudQueue idlequeue;

        // Name of queues
        private const string HIGHQ = "highq";
        private const string MEDIUMQ = "mediumq";
        private const string LOWQ = "lowq";
        private const string IDLE = "idlequeue";
        // N. of messages in queue
        private int? nofmsg = 0;
        // Tolerable wait time for tasks in queue
        private TimeSpan tolerable = TimeSpan.FromMinutes(10);

        // Max time for initial deployment
        private const int _maxdeploymenttime = 5 * 60 * 1000;
        // Max n. of roles to be deployed
        private const int _maxRoleCount = 20;
        // Initial deployment size 
        private const int _initialvmn = 3;
        // Deployment increment size
        private const int _delta = 1;

        // sampling interval for autoscaling in msec
        private const int _samplinginterval = 60 * 1000;


        // set these constants with your values to run the sample.
        private const string _serviceName = "geres15";
        private const string _deploymentSlot = "Production";
        // size of node to be deployed
        private string VMSize;

        //vm template to be used if VMs are required
        private const string _templatename = "geres15";
        //storage account name to be used
        private const string _storageacc = "<storage account name>";

        //this is the base64-encoded ManagementCertificate value out of .publishsettings file
        private const string _base64cert = "<encoded certificate>";
        private X509Certificate2 _certificate = new X509Certificate2(Convert.FromBase64String(_base64cert));
        //this is the subscription id associated to such certificate
        public const string _subscriptionId = "<subscription id>";
        //I wanted to read the values from the file but could not make it work: it is not copied to the relevant directory on debug.
        //maybe it will be on publish
        //private static string publishSettingsFilePath = "geres.publishsettings";


        //the client for the windows.azure.management api
        private ComputeManagementClient compute;


        //Main Method
        public override void Run()
        {

            // This is a sample worker implementation. Replace with your logic.
            Trace.TraceInformation("AutoScaler entry point called", "Information");

            //loop continously awaiting for messages

            while (true)
            {
                // read the queues in order of priority
                TimeSpan? waittime = TimeSpan.FromMinutes(0);
                var msg = highq.PeekMessage();
                var taskqueue = highq;
                if (msg != null)
                    waittime = DateTimeOffset.Now - msg.InsertionTime;
                if (waittime < tolerable)
                {
                    msg = mediumq.PeekMessage();
                    taskqueue = mediumq;
                    if (msg != null)
                        waittime = DateTimeOffset.Now - msg.InsertionTime;
                }
                if (waittime < tolerable)
                {
                    msg = lowq.PeekMessage();
                    taskqueue = lowq;
                    if (msg != null)
                        waittime = DateTimeOffset.Now - msg.InsertionTime;
                }

                // if there is no task waiting in the queues
                // try and see if there is an idle instance to remove
                if (msg == null)
                {
                    try
                    {
                        var idlemsg = idlequeue.GetMessage();
                        if (idlemsg != null)
                        {
                            var instancename = idlemsg.AsString;
                            Console.WriteLine("Instance {0} is idle; deleting it.", instancename);
                            Trace.TraceInformation(string.Format("Instance {0} is idle; deleting it", instancename));
                            RemoveVM(_serviceName, instancename);
                            idlequeue.DeleteMessage(idlemsg);
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine("Either there is no idle instance or there is a problem removing the VM");
                        Trace.TraceError("Either there is no idle idle instance or there is a problem removing the VM");
                        Console.WriteLine(ex.Message);
                        Trace.TraceError(string.Format(ex.Message));
                    }
                }
                else
                //there is some task in the queue, tell us if wait time is tolerable or not
                {
                    if (waittime <= tolerable)
                    {
                        Console.WriteLine("Wait time is tolerable.");
                        Trace.TraceInformation("Wait time is tolerable.");
                        Console.WriteLine("Tolerable wait time: {0}", tolerable);
                        Trace.TraceInformation(string.Format("Tolerable wait time: {0}", tolerable));
                    }
                    else
                    {
                        Console.WriteLine("Wait time is not tolerable.");
                        Trace.TraceInformation("Wait time is not tolerable.");
                        Console.WriteLine("Tolerable wait time: {0}", tolerable);
                        Trace.TraceInformation(string.Format("Tolerable wait time: {0}", tolerable));
                    }
                }

                // there is some task 

                if (msg != null)
                {
                    //if there is a message there must always be at least 1 instance to process it
                    int currentn = 0;
                    try
                    {
                        if (compute.HostedServices.GetDetailed(_serviceName).Deployments.Count == 0)
                        {
                            Console.WriteLine("No instance found. Deploying...");
                            Trace.TraceInformation("No instance found. Deploying...");
                            DeployVMs(_serviceName, _templatename, _initialvmn);
                            //give it some time to stabilize
                            Thread.Sleep(_maxdeploymenttime);
                            var result = compute.Deployments.GetBySlot(_serviceName, DeploymentSlot.Production);
                            currentn = result.RoleInstances.Count;
                        }
                        else
                        {
                            var result = compute.Deployments.GetBySlot(_serviceName, DeploymentSlot.Production);
                            currentn = result.RoleInstances.Count;
                        }

                        Console.WriteLine("Current n. of instances: {0}", currentn);
                        Trace.TraceInformation(string.Format("Current n. of instances: {0}", currentn));

                        //if there is a message, the wait time is not tolerable, there is at least one instance
                        // but less than the max, then add instances by an increment
                        if ((0 < currentn) && (currentn < _maxRoleCount) && waittime > tolerable)
                        {
                            int delta = ((currentn + _delta) < _maxRoleCount) ? _delta : 1;
                            Console.WriteLine("Adding {0} instances...", delta);
                            Trace.TraceInformation(string.Format("Adding {0} instances...", delta));
                            AddVMs(_serviceName, _templatename, delta);
                            //give it some time to stabilize
                            Thread.Sleep(_maxdeploymenttime);
                            var result = compute.Deployments.GetBySlot(_serviceName, DeploymentSlot.Production);
                            currentn = result.RoleInstances.Count;
                            Console.WriteLine("Current n. of instances: {0}", currentn);
                            Trace.TraceInformation(string.Format("Current n. of instances: {0}", currentn));
                        }
                        else
                        {
                            //if there is a message, the wait ti,e is not tolerable but we have the max instances
                            if ((waittime > tolerable) && (currentn >= _maxRoleCount))
                            {
                                Console.WriteLine("Max. number of instances reached: {0}", _maxRoleCount);
                                Trace.TraceInformation(string.Format("Max. number of instances reached: {0}", _maxRoleCount));
                            }
                        }
                    }
                    catch (CloudException Ex)
                    {
                        if (compute.HostedServices.CheckNameAvailability(_serviceName).IsAvailable)
                        {
                            //no instances exist, so create the service and deploy preset number
                            Console.WriteLine(Ex.Message);
                            Trace.TraceError(Ex.Message);
                            Console.WriteLine("No instance found. Deploying...");
                            Trace.TraceInformation("No instance found. Deploying...");
                            DeployVMs(_serviceName, _templatename, _initialvmn);
                            Thread.Sleep(_maxdeploymenttime);
                            var result = compute.Deployments.GetBySlot(_serviceName, DeploymentSlot.Production);
                            currentn = result.RoleInstances.Count;
                            Console.WriteLine("Current n. of instances: {0}", currentn);
                            Trace.TraceInformation(string.Format("Current n. of instances: {0}", currentn));
                        }
                        else
                        {
                            Console.WriteLine("Service name: {0} already in use - cannot create.", _serviceName);
                            Trace.TraceError(string.Format("Service name: {0} already in use - cannot create.", _serviceName));
                            Console.WriteLine(Ex.Message);
                            Trace.TraceError(Ex.Message);
                        }
                    }
                }
                // wait a little before re-sampling the queues
                Thread.Sleep(_samplinginterval);
            }
        }

        //create a service and deploy worker VMs in it
        private void DeployVMs(string servicename, string templatename, int vmn)
        {

            var image = compute.VirtualMachineImages.Get(templatename);

            if (compute.HostedServices.CheckNameAvailability(servicename).IsAvailable)
            {
                var cloudservice = compute.HostedServices.Create(
                    new HostedServiceCreateParameters
                    {
                        ServiceName = servicename,
                        Location = LocationNames.WestUS,
                    });
            }


            Random rnd = new Random();

            int index = 0;

            for (index = 1; index <= vmn; index++)
            {
                string computername = servicename + index.ToString();
                var configset = new ConfigurationSet
                {
                    ConfigurationSetType = ConfigurationSetTypes.WindowsProvisioningConfiguration,
                    AdminPassword = "<admin password>",
                    AdminUserName = "<admin user>",
                    ComputerName = computername,
                    HostName = string.Format(CultureInfo.InvariantCulture, "{0}.cloudapp.net", computername),
                };
                var endpoints = new ConfigurationSet
                {
                    ConfigurationSetType = ConfigurationSetTypes.NetworkConfiguration,
                    InputEndpoints = new List<InputEndpoint>
                    {
                        new InputEndpoint
                        {
                            Name = "RDP",
                            Protocol = "TCP",
                            Port = rnd.Next(50000,65000),
                            LocalPort = 3389
                        }
                    },
                };
                var vhd = new OSVirtualHardDisk
                {
                    SourceImageName = image.Name,
                    HostCaching = VirtualHardDiskHostCaching.ReadWrite,
                    MediaLink = new Uri(string.Format(CultureInfo.InvariantCulture,
                        "https://{0}.blob.core.windows.net/vhds/{1}.vhd",
                        _storageacc,
                        computername), UriKind.Absolute),
                };
                var role = new Microsoft.WindowsAzure.Management.Compute.Models.Role
                {
                    RoleName = computername,
                    RoleSize = VMSize,
                    RoleType = VirtualMachineRoleType.PersistentVMRole.ToString(),
                    OSVirtualHardDisk = vhd,
                    ConfigurationSets = new List<ConfigurationSet>
                    {
                        configset,
                        endpoints,
                    }
                };
                if (index == 1)
                {
                    var DeployPars =
                    new VirtualMachineCreateDeploymentParameters
                    {
                        Name = servicename,
                        Label = servicename,
                        DeploymentSlot = DeploymentSlot.Production,
                        Roles = new List<Microsoft.WindowsAzure.Management.Compute.Models.Role> { role }
                    };
                    var DeployResults = compute.VirtualMachines.CreateDeployment(servicename, DeployPars);
                }
                else
                {
                    var AddPars =
                           new VirtualMachineCreateParameters
                           {
                               ConfigurationSets = new List<ConfigurationSet>
                               {
                            
                                configset,
                                endpoints,
                               },
                               OSVirtualHardDisk = vhd,
                               RoleName = computername,
                               RoleSize = VMSize,
                           };

                    var AddResults = compute.VirtualMachines.Create(servicename, servicename, AddPars);

                }
            }
        }

        //Add worker VMs to an existing service .
        private void AddVMs(string servicename, string templatename, int vmn)
        {
            int currentn = 0;
            if (compute.HostedServices.GetDetailed(servicename).Deployments.Count > 0)
            {
                var result = compute.Deployments.GetBySlot(servicename, DeploymentSlot.Production);
                currentn = result.RoleInstances.Count;
            }


            var image = compute.VirtualMachineImages.Get(templatename);
            Random rnd = new Random();

            int index = 0;

            for (index = currentn + 1; index <= (currentn + vmn); index++)
            {
                string computername = servicename + index.ToString();
                var configset = new ConfigurationSet
                {
                    ConfigurationSetType = ConfigurationSetTypes.WindowsProvisioningConfiguration,
                    AdminPassword = "<admin password>",
                    AdminUserName = "<admin user>",
                    ComputerName = computername,
                    HostName = string.Format(CultureInfo.InvariantCulture, "{0}.cloudapp.net", computername),
                };
                var endpoints = new ConfigurationSet
                {
                    ConfigurationSetType = ConfigurationSetTypes.NetworkConfiguration,
                    InputEndpoints = new List<InputEndpoint>
                    {
                        new InputEndpoint
                        {
                            Name = "RDP",
                            Protocol = "TCP",
                            Port = rnd.Next(50000,65000),
                            LocalPort = 3389
                        }
                    },
                };
                var vhd = new OSVirtualHardDisk
                {
                    SourceImageName = image.Name,
                    HostCaching = VirtualHardDiskHostCaching.ReadWrite,
                    MediaLink = new Uri(string.Format(CultureInfo.InvariantCulture,
                        "https://{0}.blob.core.windows.net/vhds/{1}.vhd",
                        _storageacc,
                        computername), UriKind.Absolute),
                };
                var AddPars =
                           new VirtualMachineCreateParameters
                           {
                               ConfigurationSets = new List<ConfigurationSet>
                               {
                            
                                configset,
                                endpoints,
                               },
                               OSVirtualHardDisk = vhd,
                               RoleName = computername,
                               RoleSize = VMSize,
                           };

                var AddResults = compute.VirtualMachines.Create(servicename, servicename, AddPars);
            }

        }

        //Remove worker VMs from an existing service.
        private void RemoveVM(string servicename, string vmname)
        {
            try
            {
                var result = compute.Deployments.GetBySlot(servicename, DeploymentSlot.Production);
                int currentn = result.RoleInstances.Count;
                if (currentn > 1)
                {
                    compute.VirtualMachines.Delete(servicename, servicename, vmname, deleteFromStorage: true);
                    Console.WriteLine("Instance {0} was deleted.", vmname);
                    Trace.TraceInformation(string.Format("Instance {0} was deleted.", vmname));
                }
                else
                {
                    compute.Deployments.DeleteBySlot(servicename, DeploymentSlot.Production);
                    Console.WriteLine("Deleted last instance {0}.", vmname);
                    Trace.TraceInformation(string.Format("Deleted last instance {0}.", vmname));
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("No instance to delete.");
                Trace.TraceInformation("No instance to delete.");
                Trace.TraceWarning(ex.Message);
            }

        }


        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections 
            ServicePointManager.DefaultConnectionLimit = 12;

            // Read the storage account connection settings
            var StorageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("DataConnectionString"));
            string size = CloudConfigurationManager.GetSetting("VMSize");
            switch (size)
            {
                case "Small":

                    this.VMSize = VirtualMachineRoleSize.Small;
                    break;

                case "Medium":

                    this.VMSize = VirtualMachineRoleSize.Medium;
                    break;

                case "Large":

                    this.VMSize = VirtualMachineRoleSize.Large;
                    break;

                case "ExtraLarge":

                    this.VMSize = VirtualMachineRoleSize.ExtraLarge;
                    break;

                case "A5":

                    this.VMSize = VirtualMachineRoleSize.A5;
                    break;

                case "A6":

                    this.VMSize = VirtualMachineRoleSize.A6;
                    break;

                case "A7":

                    this.VMSize = VirtualMachineRoleSize.A7;
                    break;

                case "A8":

                    this.VMSize = VirtualMachineRoleSize.A8;
                    break;

                case "A9":

                    this.VMSize = VirtualMachineRoleSize.A9;
                    break;

                default:

                    this.VMSize = VirtualMachineRoleSize.Small;
                    break;
            }


            CloudQueueClient queueclient = StorageAccount.CreateCloudQueueClient();
            this.highq = queueclient.GetQueueReference(HIGHQ);
            highq.CreateIfNotExists();
            this.mediumq = queueclient.GetQueueReference(MEDIUMQ);
            mediumq.CreateIfNotExists();
            this.lowq = queueclient.GetQueueReference(LOWQ);
            lowq.CreateIfNotExists();
            this.idlequeue = queueclient.GetQueueReference(IDLE);
            idlequeue.CreateIfNotExists();

            // Get a Service management connection
            CertificateCloudCredentials cloudcredentials = new CertificateCloudCredentials(_subscriptionId, _certificate);
            ManagementClient datacenter = CloudContext.Clients.CreateManagementClient(cloudcredentials);
            this.compute = CloudContext.Clients.CreateComputeManagementClient(cloudcredentials);

            return base.OnStart();
        }


    }
}
